package com.nulldev.util.web.HttpServer3.util;

import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;

import com.nulldev.util.JVM.JVM;
import com.nulldev.util.JVM.LegacyReflectionUtils;
import com.nulldev.util.VariableAPI.ByteUtil;
import com.nulldev.util.io.IOUtils;
import com.nulldev.util.manage.MarkedForRemoval;

public class MarkableInputStream extends InputStream {
	private final InputStream in;

	private long offset;
	private long reset;
	private long limit;

	private long defaultMark = -1;

	public MarkableInputStream(InputStream in) {
		if (!in.markSupported()) {
			in = new BetterBufferedInputStream(in);
		}
		this.in = in;
	}

	public MarkableInputStream(InputStream in, final int cacheSize) {
		if (!in.markSupported()) {
			in = new BetterBufferedInputStream(in, cacheSize);
		}
		this.in = in;
	}

	public boolean changeCacheSize(final int cacheSize) {
		if (cacheSize < 1)
			throw new IllegalArgumentException("cacheSize < 1!");
		try {
			if (this.in instanceof BetterBufferedInputStream) {
				((BetterBufferedInputStream) this.in).changeCacheSize(cacheSize);
			} else if (this.in instanceof MarkableInputStream) {
				((MarkableInputStream) this.in).changeCacheSize(cacheSize);
			} else if (this.in instanceof BufferedInputStream) {
				final byte[] s = LegacyReflectionUtils.getField(this.in, "buf");
				LegacyReflectionUtils.setField(this.in, "buf", ByteUtil.concatenateByteArrays(s, new byte[cacheSize - s.length]));
				return true;
			}
		} catch (Exception e) {
			JVM.errorln(e);
		}
		return false;
	}

	/** Marks this place in the stream so we can reset back to it later. */
	@Override
	public void mark(int readLimit) {
		defaultMark = savePosition(readLimit);
	}

	/**
	 * Returns an opaque token representing the current position in the stream. Call
	 * {@link #reset(long)} to return to this position in the stream later. It is an
	 * error to call {@link #reset(long)} after consuming more than
	 * {@code readLimit} bytes from this stream.
	 */
	public long savePosition(int readLimit) {
		long offsetLimit = offset + readLimit;
		if (limit < offsetLimit) {
			setLimit(offsetLimit);
		}
		return offset;
	}

	/**
	 * Makes sure that the underlying stream can backtrack the full range from
	 * {@code reset} thru {@code limit}. Since we can't call {@code mark()} without
	 * also adjusting the reset-to-position on the underlying stream this method
	 * resets first and then marks the union of the two byte ranges. On buffered
	 * streams this additional cursor motion shouldn't result in any additional I/O.
	 */
	private void setLimit(long limit) {
		try {
			if (reset < offset && offset <= this.limit) {
				in.reset();
				in.mark((int) (limit - reset));
				skip(reset, offset);
			} else {
				reset = offset;
				in.mark((int) (limit - offset));
			}
			this.limit = limit;
		} catch (IOException e) {
			throw new IllegalStateException("Unable to mark: " + e);
		}
	}

	/** Resets the stream to the most recent {@link #mark mark}. */
	@Override
	public void reset() throws IOException {
		reset(defaultMark);
	}

	/** Resets the stream to the position recorded by {@code token}. */
	public void reset(long token) throws IOException {
		if (offset > limit || token < reset) {
			throw new IOException("Cannot reset");
		}
		in.reset();
		skip(reset, token);
		offset = token;
	}

	/** Skips {@code target - current} bytes and returns. */
	private void skip(long current, long target) throws IOException {
		while (current < target) {
			long skipped = in.skip(target - current);
			if (skipped == 0) {
				if (read() == -1) {
					break; // EOF
				} else {
					skipped = 1;
				}
			}
			current += skipped;
		}
	}

	@Override
	public int read() throws IOException {
		int result = in.read();
		if (result != -1) {
			offset++;
		}
		return result;
	}

	@Override
	public int read(byte[] buffer) throws IOException {
		int count = in.read(buffer);
		if (count != -1) {
			offset += count;
		}
		return count;
	}

	@Override
	public int read(byte[] buffer, int offset, int length) throws IOException {
		int count = in.read(buffer, offset, length);
		if (count != -1) {
			this.offset += count;
		}
		return count;
	}

	@Override
	public long skip(final long byteCount) throws IOException {
		long skipped = in.skip(byteCount);
		offset += skipped;
		return skipped;
	}

	@Override
	public int available() throws IOException {
		return in.available();
	}

	@Override
	public void close() throws IOException {
		in.close();
	}

	@Override
	public boolean markSupported() {
		return in.markSupported();
	}

	public byte[] getCache() {
		if (this.in instanceof MarkableInputStream)
			return ((MarkableInputStream) this.in).getCache();
		if (this.in instanceof BetterBufferedInputStream)
			return ((BetterBufferedInputStream) this.in).buf;
		throw new UnsupportedOperationException("Cannot perform on: " + in);
	}

	public byte[] flushCache() {
		if (this.in instanceof MarkableInputStream) {
			IOUtils.closeQuietly(this);
			return ((MarkableInputStream) this.in).getCache();
		}
		if (this.in instanceof BetterBufferedInputStream) {
			IOUtils.closeQuietly(this);
			return ((BetterBufferedInputStream) this.in).buf;
		}
		throw new UnsupportedOperationException("Cannot perform on: " + in);
	}

	public MarkableInputStream importBuffer(final byte[] initData) {
		if (this.in instanceof MarkableInputStream)
			((MarkableInputStream) this.in).importBuffer(initData);
		if (this.in instanceof BetterBufferedInputStream)
			((BetterBufferedInputStream) this.in).buf = initData;
		return this;
	}

	@Deprecated
	@MarkedForRemoval(reason = "Compat. for classes affected by the InputReader removal.")
	public String nextLine() throws IOException {
		final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(IOUtils.STRING_ALLOC_BUFFER);
		int c;
		while ((c = read()) != -1) { // BufferedReader
			if (c == ((char) '\r') || c == ((char) '\n')) {
				if (c == ((char) '\r'))
					read();
				break;
			}
			byteArrayOutputStream.write(c);
			if (byteArrayOutputStream.size() + 1 > 65535) {
				break;
			}
		}
		if (c == -1) {
			return null;
		}
		final String line = byteArrayOutputStream.toString("UTF-8");
		IOUtils.closeQuietly(byteArrayOutputStream);
		return line;
	}
}
